// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © M1D
//@version=6
indicator("Auto OTE+ (M1D)", "OTE+", overlay = true, max_lines_count = 50, max_labels_count = 50, max_boxes_count = 10, max_bars_back = 2000)

import TradingView/ZigZag/7 as zigzag

// ─── Constants ───────────────────────────────────────────────────────────────
const color C_BULL = #1d4ed8
const color C_BEAR = #f23645
const color MID    = #585a63
const color CARD   = #131722
const color CARD_T = #D1D4DC
const string NY    = "America/New_York"

// ─── Inputs ──────────────────────────────────────────────────────────────────
const string G_SW = "Swings"
const string G_OT = "OTE"
const string G_ST = "Style"
const string G_TB = "Status card"

float  devMult   = input.float(2.5, "Deviation", minval = 0.0, step = 0.5, group = G_SW, tooltip = "How far price must move against the last swing, in multiples of ATR(10) as a % of price, before a new swing is confirmed. Same engine as TradingView's Auto Fib Retracement (its default is 3). 2.5 measures the intraday swing; 3 picks bigger legs such as the overnight move; 2 and below follow every small bounce.")
int    depth     = input.int(10, "Depth", minval = 2, group = G_SW, tooltip = "The minimum bars between swings. Same default as TradingView's Auto Fib Retracement.")
float  oteFrom   = input.float(0.62, "OTE from", minval = 0.0, maxval = 1.0, step = 0.01, group = G_OT)
float  oteTo     = input.float(0.79, "OTE to", minval = 0.0, maxval = 1.0, step = 0.01, group = G_OT)
bool   showLeg   = input.bool(true, "Show the measured leg", group = G_OT, tooltip = "A dotted line from the swing's start to its extreme, so you can see what the OTE is measured on.")
bool   showLvls  = input.bool(true, "Ratio lines 1 / 0.79 / 0.705 / 0.62 / 0.5 / 0", group = G_OT)
bool   showTgt   = input.bool(true, "Targets -0.27 / -0.68 / -1 / -2 / -3 STD", group = G_OT, tooltip = "The leg projected beyond its extreme: 27%, 68%, then one, two and three full leg lengths (standard deviations).")
int    fillTr    = input.int(85, "Band fill transparency", minval = 0, maxval = 100, group = G_ST)
int    runway    = input.int(10, "Right offset (bars)", minval = 0, maxval = 100, group = G_ST)
string lblSize   = input.string("small", "Label text size", options = ["tiny", "small", "normal"], group = G_ST)
bool   showTbl   = input.bool(true, "Show status card", group = G_TB)
string tblPos    = input.string("Top Right", "Position", options = ["Top Right", "Bottom Right", "Top Left", "Bottom Left"], group = G_TB)

f_size(string s) =>
    switch s
        "tiny"   => size.tiny
        "normal" => size.normal
        =>          size.small

string SZ = f_size(lblSize)

// ─── Swings (TradingView ZigZag, the Auto Fib engine) ────────────────────────
var zigzag.Settings zs = zigzag.Settings.new(devMult, depth, color(na), false, false, false, false, "Absolute", true)
var zigzag.ZigZag   zz = zigzag.newInstance(zs)
zs.devThreshold := ta.atr(10) / close * 100 * devMult
zz.update()

//@function The k-th newest leg (0 = the one still forming).
//@param k (int) how many legs back
//@returns (zigzag.Pivot) the leg, or na
f_leg(int k) =>
    int n = zz.pivots.size()
    zigzag.Pivot out = na
    if n > k
        zigzag.Pivot lp = zz.lastPivot()
        bool pushed = not na(lp) and zz.pivots.last().end.time == lp.end.time
        out := pushed ? zz.pivots.get(n - 1 - k) : zz.pivots.get(k)
    out

// The OTE belongs to the IMPULSE. If the newest leg is shorter than the one before it,
// the newest leg is the retracement and the OTE is measured on the leg before it.
zigzag.Pivot newest = f_leg(0)
zigzag.Pivot prior  = f_leg(1)
bool  ready   = not na(newest) and not na(prior)
bool  onPrior = ready and math.abs(newest.end.price - newest.start.price) < math.abs(prior.end.price - prior.start.price)
float sP      = ready ? (onPrior ? prior.start.price : newest.start.price) : na
float eP      = ready ? (onPrior ? prior.end.price : newest.end.price) : na
int   sT      = ready ? (onPrior ? prior.start.time : newest.start.time) : na
int   eT      = ready ? (onPrior ? prior.end.time : newest.end.time) : na
bool  isLong  = eP > sP
float legSz   = math.abs(eP - sP)

//@function The price at a retracement ratio of the leg: 0 = its extreme, 1 = its start.
f_lvl(float f) =>
    eP + (sP - eP) * f

float bandHi = ready ? math.max(f_lvl(oteFrom), f_lvl(oteTo)) : na
float bandLo = ready ? math.min(f_lvl(oteFrom), f_lvl(oteTo)) : na
bool  inBand = ready and high >= bandLo and low <= bandHi
bool  past   = ready and (isLong ? low < f_lvl(oteTo) : high > f_lvl(oteTo))
bool  broken = ready and (isLong ? low < sP : high > sP)

//@function The deepest retracement since the leg's extreme, read fresh from the bars (no carried state).
//@returns (float) the price furthest back toward the leg's start, or na
f_deep() =>
    float d = na
    int i = 0
    while i < 1999 and time[i] > eT
        d := na(d) ? (isLong ? low[i] : high[i]) : isLong ? math.min(d, low[i]) : math.max(d, high[i])
        i += 1
    d

//@function Which kill zone a time falls in (NY clock).
f_kz(int t) =>
    int m = hour(t, NY) * 60 + minute(t, NY)
    m >= 18 * 60 ? "Asia" : m >= 120 and m < 300 ? "London" : m >= 420 and m < 540 ? "NY AM" : m >= 570 and m < 660 ? "NY open" : "between KZs"

// ─── Drawing ─────────────────────────────────────────────────────────────────
var box   bx   = na
var line  legL = na
var table tbl  = na
var array<line>  lvL = array.new<line>()
var array<label> lvB = array.new<label>()

f_pos(string p) =>
    switch p
        "Bottom Right" => position.bottom_right
        "Top Left"     => position.top_left
        "Bottom Left"  => position.bottom_left
        =>                position.top_right

//@function One ratio line and its name at the right end, text in the line's colour.
f_level(int x1, int x2, float f, string name, color c, string ln) =>
    float y = f_lvl(f)
    lvL.push(line.new(x1, y, x2, y, xloc = xloc.bar_time, color = c, width = 1, style = ln))
    label lb = label.new(x2, y, name, xloc = xloc.bar_time, style = label.style_label_left, color = color.new(#FFFFFF, 100), textcolor = c, size = SZ, textalign = text.align_left, tooltip = str.tostring(f) + " = " + str.tostring(y, format.mintick))
    label.set_text_font_family(lb, font.family_monospace)
    lvB.push(lb)

f_row(int r, string k, string v, color vc) =>
    table.cell(tbl, 0, r, k, text_color = #B2B5BE, text_size = SZ, text_halign = text.align_left, text_font_family = font.family_monospace)
    table.cell(tbl, 1, r, v, text_color = vc, text_size = SZ, text_halign = text.align_right, text_font_family = font.family_monospace)

if barstate.islast
    for l in lvL
        line.delete(l)
    for b in lvB
        label.delete(b)
    lvL.clear()
    lvB.clear()
    if ready
        int   xEnd = time + runway * timeframe.in_seconds() * 1000
        color base = isLong ? C_BULL : C_BEAR
        color fill = color.new(base, fillTr)
        // The pocket
        if na(bx)
            bx := box.new(eT, bandHi, xEnd, bandLo, xloc = xloc.bar_time, border_color = fill, border_width = 1, bgcolor = fill)
        else
            box.set_lefttop(bx, eT, bandHi)
            box.set_rightbottom(bx, xEnd, bandLo)
            box.set_bgcolor(bx, fill)
            box.set_border_color(bx, fill)
        // The leg being measured
        if showLeg
            if na(legL)
                legL := line.new(sT, sP, eT, eP, xloc = xloc.bar_time, color = base, width = 1, style = line.style_dotted)
            else
                line.set_xy1(legL, sT, sP)
                line.set_xy2(legL, eT, eP)
                line.set_color(legL, base)
        else
            line.delete(legL)
            legL := na
        // Ratio lines, from the extreme to the right
        if showLvls
            f_level(sT, xEnd, 1.0, "1", MID, line.style_dotted)
            f_level(eT, xEnd, oteTo, str.tostring(oteTo), base, line.style_dotted)
            f_level(eT, xEnd, 0.705, "0.705 ⁘ OTE", base, line.style_solid)
            f_level(eT, xEnd, oteFrom, str.tostring(oteFrom), base, line.style_dotted)
            f_level(eT, xEnd, 0.5, "0.5 EQ", MID, line.style_dotted)
            f_level(eT, xEnd, 0.0, "0", MID, line.style_dotted)
        if showTgt
            for f in array.from(-0.27, -0.68, -1.0, -2.0, -3.0)
                f_level(eT, xEnd, f, f <= -1.0 ? str.tostring(f) + " STD" : str.tostring(f), MID, line.style_dotted)

        // Status
        float deep   = f_deep()
        float ratio  = legSz > 0 and not na(deep) ? (isLong ? eP - deep : deep - eP) / legSz : 0.0
        float nowR   = legSz > 0 ? (isLong ? eP - close : close - eP) / legSz : 0.0
        string pocket = ratio > 1.0 ? "failed" : ratio > oteTo ? "past 0.79" : ratio >= oteFrom ? (nowR < oteFrom ? "held" : "tapped") : ratio >= 0.5 ? "at EQ" : "waiting"
        color  pc     = pocket == "failed" ? C_BEAR : pocket == "held" ? #089981 : pocket == "tapped" ? #F7A600 : CARD_T
        if showTbl
            if na(tbl)
                tbl := table.new(f_pos(tblPos), 2, 7, bgcolor = CARD, border_width = 0, frame_color = CARD, frame_width = 4)
            table.cell(tbl, 0, 0, "OTE", text_color = CARD_T, text_size = SZ, text_halign = text.align_left, text_font_family = font.family_monospace)
            table.cell(tbl, 1, 0, isLong ? "BULL" : "BEAR", text_color = isLong ? #5B8CFF : C_BEAR, text_size = SZ, text_halign = text.align_right, text_font_family = font.family_monospace)
            f_row(1, "POCKET", pocket, pc)
            f_row(2, "LEG", str.tostring(sP, format.mintick) + " → " + str.tostring(eP, format.mintick), CARD_T)
            f_row(3, "SIZE", str.tostring(legSz, format.mintick) + " pts", CARD_T)
            f_row(4, "FROM", str.format_time(sT, "EEE HH:mm", NY) + " → " + str.format_time(eT, "HH:mm", NY), CARD_T)
            f_row(5, "SESSION", f_kz(eT), CARD_T)
            f_row(6, "RETRACE", str.tostring(ratio * 100, "0") + "% deepest · " + str.tostring(nowR * 100, "0") + "% now", CARD_T)
        else if not na(tbl)
            table.delete(tbl)
            tbl := na
    else
        box.delete(bx)
        line.delete(legL)
        bx := na
        legL := na

// ─── Alerts ──────────────────────────────────────────────────────────────────
alertcondition(inBand and not inBand[1], "Price in the OTE", "{{ticker}}: price traded into the OTE.")
alertcondition(past and not past[1], "Price past the OTE", "{{ticker}}: price went through the OTE (past 0.79).")
alertcondition(broken and not broken[1], "Leg broken", "{{ticker}}: price went back through the start of the OTE leg.")
